/**
 * TaskGenerator.java
 * Created on Jul 17, 2009
 */

package sim.tasks;

import java.awt.geom.Point2D;
import java.util.ArrayList;
import java.util.Arrays;
import sim.component.VisiblePlayer;
import sim.*;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import scio.coordinate.utils.PlanarMathUtils;
import sim.component.PhantomAgent;
import sim.component.agent.Agent;
import sim.component.team.Team;
import sim.tasks.Task.Type;

/**
 * <p>
 *   <code>TaskGenerator</code> generates a set of tasks given the current <code>DistanceTable</code>,
 *   list of known opponents, and list of teammates.
 * </p>
 *
 * @author Elisha Peterson
 */
public abstract class Tasker {

    /** 
     * Creates several tasks for a team, based on specified parameters.
     * @param dt the table of distances between agents in the simulation
     * @param owner the class calling the generator
     * @param visibleOpponents the opponents visible to the calling class
     * @param teammates the teammates of the calling class
     * @return list of tasks for this agent and its teammates
     */
    abstract public List<Task> generateGiven(
            DistanceCache dt,
            Team owner,
            Collection<? extends VisiblePlayer> visibleOpponents);

    /**
     * Creates several tasks for a single agent, based on the specified parameters.
     * If the procedure does not produce any tasks, it should return an empty list,
     * not a null value. Note that tasks whose target is the same as the agent's position
     * will not work properly with other classes, particular the gradient <code>TaskChooser</code>,
     * so rather than returning a task with the agent's position, the method should
     * return an empty list.
     *
     * @param dt the table of distances between agents in the simulation
     * @param owner the class calling the generator
     * @param visibleOpponents the opponents visible to the calling class
     * @param teammates the teammates of the calling class
     * @return list of tasks for this agent and its teammates
     */
    abstract public List<Task> generateGiven(
            DistanceCache dt,
            Agent owner,
            Collection<? extends VisiblePlayer> visibleOpponents,
            Collection<Agent> teammates);

    /** @return the opposing team that is the focus of this task generator. */
    abstract public List<? extends VisiblePlayer> getTargetAgents();

    /** @param newTarget the new target team for this task/generator. */
    abstract public void setTargetAgents(Collection<? extends VisiblePlayer> newTarget);

    /** @return default priority value for tasks generated by this method. */
    abstract public double getDefaultPriority();

    /** @param newValue the new value for the default priority. */
    abstract public void setDefaultPriority(double newValue);

    /** @return enum value representing the type of task generated by this class. */
    abstract public Task.Type getTaskType();



    //============================================//
    //                                            //
    //               FACTORY METHODS              //
    //                                            //
    //============================================//


    /** An enum encoding possible generator values */
    public enum TaskerEnum { 
        SPIRAL_SEARCH,
        FOLLOW,
        CLOSEST,
        CLOSEST_TWO,
        CENTER_OF_MASS,
        GRADIENT,
        AVERAGE_HEADING,
        WEIGHTED_AVERAGE_HEADING,
        CONTROL_CLOSEST,
        CONTROL_OPTIMAL
    };

    /** Retrieves an instance of the generator given an enum value */
    public static Tasker getInstance(TaskerEnum value) {
        switch (value) {
            case SPIRAL_SEARCH:
                return new SpiralSearch();
            case FOLLOW:
                return new Follow();
            case CLOSEST:
                return new Closest();
            case CENTER_OF_MASS:
                return new CenterOfMass();
            case GRADIENT:
                return new Gradient();
            case AVERAGE_HEADING:
                return new AverageHeading();
            case WEIGHTED_AVERAGE_HEADING:
                return new AverageHeadingWeighted();
            case CONTROL_CLOSEST:
                return new ControlClosest();
            case CONTROL_OPTIMAL:
                return new ControlOptimal();
            default:
                return new Follow();
        }
    }
    

    //============================================//
    //                                            //
    //               INNER CLASSES                //
    //                                            //
    //============================================//


    /** Behavior where the target seeks out an opponent. */
    abstract static class BasicSeekTasker extends Tasker {
        double priority = 1.0;
        Task.Type type = Task.Type.SEEK;

        public BasicSeekTasker() {}
        public BasicSeekTasker(Task.Type type) { setTaskType(type); }
        public BasicSeekTasker(Task.Type type, double priority) { setTaskType(type); setDefaultPriority(priority); }
        @Override public String toString() { return String.format("Tasker[priority=%.2f].", this.priority); }

        public double getDefaultPriority() { return priority; }
        public void setDefaultPriority(double newValue) {
            if (newValue < 0)
                throw new IllegalArgumentException("priority < 0: " + newValue);
            this.priority = newValue;
        }

        public Task.Type getTaskType() { return type; }
        public void setTaskType(Task.Type type) { this.type = type; }

        public String getTaskTypeByName() { return type==null ? "" : type.name(); }
        public void setTaskTypeByName(String name) {
            try {
                type = Task.Type.valueOf(name);
            } catch (IllegalArgumentException ex) {
            }
        }

        public List<? extends VisiblePlayer> getTargetAgents() { return Collections.emptyList(); }
        public void setTargetAgents(Collection<? extends VisiblePlayer> newTarget) { }

        /**
         * Convenience method. Generates a list of tasks for sub-agents within the team, by
         * calling the task generator on each agent within the team. If the behavior of the
         * task generator is independent of the team's structure, they may wish to use this
         * method to obtain the list of tasks.
         * @param tg the task generator to use
         * @param dt the cache of distances between agents
         * @param owner the team generating the tasks
         * @param visibleOpponents the opponents visible to the team
         * @return list of tasks for the entire team
         */
        public List<Task> generateGiven(DistanceCache dt, Team owner, Collection<? extends VisiblePlayer> visibleOpponents) {
            ArrayList<Task> result = new ArrayList<Task>();
            List<Agent> team = owner.state.activeAgents;
            for (Agent a : team)
                result.addAll(generateGiven(dt, a, visibleOpponents, team));
            return result;
        }
    } // INNER CLASS Tasker.BasicSeekTasker


    /** This class implements default behavior for lots of the methods in Tasker. */
    abstract static class BasicTargetTasker extends BasicSeekTasker {
        List<VisiblePlayer> targetAgents;

        public BasicTargetTasker() { targetAgents = new ArrayList<VisiblePlayer>(); }
        public BasicTargetTasker(Collection<? extends VisiblePlayer> targetAgents, Task.Type type) { this(targetAgents, type, 1.0); }
        public BasicTargetTasker(Collection<? extends VisiblePlayer> targetAgents, Task.Type type, double priority) {
            super(type, priority);
            if (targetAgents instanceof List)
                this.targetAgents = (List) targetAgents;
            else
                this.targetAgents = new ArrayList<VisiblePlayer>(targetAgents);
        }

        @Override
        public List<? extends VisiblePlayer> getTargetAgents() { return targetAgents; }
        @Override
        public void setTargetAgents(Collection<? extends VisiblePlayer> newTarget) {
            if (newTarget == null)
                throw new IllegalArgumentException("targetAgents null");
            targetAgents.clear();
            targetAgents.addAll(newTarget);
        }
    } // INNER CLASS Tasker.AbstractTasker

    
    /** Represents a task where the agent "follows" the first opponent agent. */
    public static class SpiralSearch extends BasicSeekTasker {
        @Override public String toString() { return super.toString() + "SpiralSearch"; }
        @Override
        public List<Task> generateGiven(DistanceCache dt, Agent owner, Collection<? extends VisiblePlayer> visibleOpponents, Collection<Agent> teammates) {
            throw new UnsupportedOperationException("Not supported yet.");
        }
    } // INNER CLASS Tasker.SpiralSearch
    

    /** Represents a task where the agent "follows" the first opponent agent. */
    public static class Follow extends BasicTargetTasker {
        public Follow(){}
        public Follow(VisiblePlayer target, Task.Type type) { targetAgents.add(target); setTaskType(type); }
        public Follow(VisiblePlayer target, Task.Type type, double priority) { targetAgents.add(target); setTaskType(type); setDefaultPriority(priority); }
        @Override public String toString() { return super.toString() + "Follow"; }

        @Override
        public List<Task> generateGiven(DistanceCache dt, Team owner, Collection<? extends VisiblePlayer> visibleOpponents) {
            ArrayList<Task> result = new ArrayList<Task>();
            VisiblePlayer target = null;
            for (VisiblePlayer vp : targetAgents)
                if (visibleOpponents.contains(vp)) {
                    target = vp;
                    break;
                }
            if (target == null)
                return Collections.EMPTY_LIST;
            for (VisiblePlayer sc : owner.state.activeAgents)
                if (sc instanceof Agent && !((Agent)sc).state.position.equals(target.getPosition()))
                    result.add(new Task((Agent) sc, target, priority, type));
            return result;
        }

        public List<Task> generateGiven(DistanceCache dt, Agent owner, Collection<? extends VisiblePlayer> visibleOpponents, Collection<Agent> teammates) {
            ArrayList<Task> result = new ArrayList<Task>();
            VisiblePlayer target = null;
            for (VisiblePlayer vp : targetAgents)
                if (visibleOpponents.contains(vp)) {
                    target = vp;
                    break;
                }
            if (target == null)
                return Collections.EMPTY_LIST;
            result.add(new Task(owner, target, priority, type));
            return result;
        }
    } // INNER CLASS Tasker.Follow


    /** Each player follows the closest target. */
    public static class Closest extends BasicTargetTasker {
        public Closest() {}
        public Closest(Collection<? extends VisiblePlayer> targetAgents, Task.Type type) { super(targetAgents, type); }
        public Closest(Collection<? extends VisiblePlayer> targetAgents, Type type, double priority) { super(targetAgents, type, priority); }
        @Override public String toString() { return super.toString() + "Closest"; }

        public List<Task> generateGiven(DistanceCache dt, Agent owner, Collection<? extends VisiblePlayer> visibleOpponents, Collection<Agent> teammates) {
            // reduce to visible opponents only
            HashSet<VisiblePlayer> visOpps = new HashSet<VisiblePlayer>(visibleOpponents);
            visOpps.retainAll(targetAgents);
            if (visOpps.size() == 0)
                return Collections.emptyList();

            VisiblePlayer aspTarget = dt.getClosestAgent(owner, visOpps);

            if (aspTarget == null || owner.state.position.equals(aspTarget.getPosition()))
                return Collections.emptyList();

            return Arrays.asList(new Task(owner, aspTarget, priority, type));
        }
    } // INNER CLASS Tasker.Closest


    /** Each player follows the closest target. */
    public static class ClosestTwo extends BasicTargetTasker {
        public ClosestTwo() {}
        public ClosestTwo(Collection<? extends VisiblePlayer> targetAgents, Task.Type type) { super(targetAgents, type); }
        public ClosestTwo(Collection<? extends VisiblePlayer> targetAgents, Type type, double priority) { super(targetAgents, type, priority); }
        @Override public String toString() { return super.toString() + "ClosestTwo"; }

        public List<Task> generateGiven(DistanceCache dt, Agent owner, Collection<? extends VisiblePlayer> visibleOpponents, Collection<Agent> teammates) {
            // reduce to visible opponents only
            HashSet<VisiblePlayer> visOpps = new HashSet<VisiblePlayer>(visibleOpponents);
            visOpps.retainAll(targetAgents);
            if (visOpps.size() == 0)
                return Collections.emptyList();

            // TODO - implement new logic here
            return Collections.emptyList();
        }
    } // INNER CLASS Tasker.ClosestTwo



    /** Each player seeks the center-of-mass of the target group. */
    public static class CenterOfMass extends BasicTargetTasker {
        public CenterOfMass() {}
        public CenterOfMass(Collection<? extends VisiblePlayer> targetAgents, Task.Type type) { super(targetAgents, type); }
        public CenterOfMass(Collection<? extends VisiblePlayer> targetAgents, Type type, double priority) { super(targetAgents, type, priority); }
        @Override public String toString() { return super.toString() + "CenterOfMass"; }

        public List<Task> generateGiven(DistanceCache dt, Agent owner, Collection<? extends VisiblePlayer> visibleOpponents, Collection<Agent> teammates) {
            if (visibleOpponents.size() == 0)
                return Collections.emptyList();

            // finds the center of mass
            Point2D.Double com = new Point2D.Double();
            int num = 0;
            for (VisiblePlayer asp : visibleOpponents) {
                if (asp == owner || !targetAgents.contains(asp))
                    continue;
                PlanarMathUtils.translate(com, asp.getPosition());
            }
            if (num == 0)
                return Collections.emptyList();
            com.x /= num;
            com.y /= num;

            return Arrays.asList(new Task(owner, new PhantomAgent(com, PlanarMathUtils.ZERO), priority, type));
        }
    } // INNER CLASS Tasker.CenterOfMass




    /** Each player minimizes/maximizes the sum of distances to targets via a gradient calculation. */
    public static class Gradient extends BasicTargetTasker {
        public Gradient() {}
        public Gradient(Collection<? extends VisiblePlayer> targetAgents, Task.Type type) { super(targetAgents, type); }
        public Gradient(Collection<? extends VisiblePlayer> targetAgents, Type type, double priority) { super(targetAgents, type, priority); }
        @Override public String toString() { return super.toString() + "Gradient"; }

        public List<Task> generateGiven(DistanceCache dt, Agent owner, Collection<? extends VisiblePlayer> visibleOpponents, Collection<Agent> teammates) {
            if (visibleOpponents.size() == 0)
                return Collections.emptyList();

            Point2D.Double loc = owner.state.position;

            // minimize sum of distances
            Point2D.Double dir = new Point2D.Double();
            double dx, dy, multiplier;
            Point2D.Double bPos = null;
            for (VisiblePlayer b : visibleOpponents) {
                if (! targetAgents.contains(b) )
                    continue;
                bPos = b.getPosition();
                dx = (bPos.x - loc.x);
                dy = (bPos.y - loc.y);
                multiplier = 1 / (dx*dx + dy*dy);
                dir.x += dx * multiplier;
                dir.y += dy * multiplier;
            }
            if (bPos == null) // found nothing
                return Collections.emptyList();

            // return task with phantom agent target
            Point2D.Double tLoc = new Point2D.Double(loc.x + dir.x, loc.y + dir.y);
            return Arrays.asList(new Task(owner, new PhantomAgent(tLoc, PlanarMathUtils.ZERO), priority, type));
        }
    } // INNER CLASS Tasker.Gradient




    /** Each player approximates the heading of the targets. */
    public static class AverageHeading extends BasicTargetTasker {
        public AverageHeading() {}
        public AverageHeading(Collection<? extends VisiblePlayer> targetAgents, Task.Type type) { super(targetAgents, type); }
        public AverageHeading(Collection<? extends VisiblePlayer> targetAgents, Type type, double priority) { super(targetAgents, type, priority); }
        @Override public String toString() { return super.toString() + "AverageHeading"; }

        public List<Task> generateGiven(DistanceCache dt, Agent owner, Collection<? extends VisiblePlayer> visibleOpponents, Collection<Agent> teammates) {
            if (visibleOpponents.size() == 0)
                return Collections.emptyList();

            // finds the center of mass by averaging all headings
            Point2D.Double avgHeading = new Point2D.Double();
            int num = 0;
            for (VisiblePlayer asp : visibleOpponents) {
                if (!targetAgents.contains(asp) || asp == owner)
                    continue;
                num++;
                if (asp.getVelocity() == null)
                    continue;
                PlanarMathUtils.translate(avgHeading, PlanarMathUtils.normalize((Point2D.Double) asp.getVelocity().clone()));
            }
            if (num == 0 || (avgHeading.x == 0  && avgHeading.y == 0))
                return Collections.emptyList();
            
            avgHeading.x /= num;
            avgHeading.y /= num;
            
            // return task with phantom agent target
            Point2D.Double tLoc = new Point2D.Double(owner.state.position.x + avgHeading.x, owner.state.position.y + avgHeading.y);
            return Arrays.asList(new Task(owner, new PhantomAgent(tLoc, PlanarMathUtils.ZERO), priority, type));
        }
    } // INNER CLASS Tasker.AverageHeading

    
    /** Each player approximates the heading of the targets, with closer targets weighted more heavily. */
    public static class AverageHeadingWeighted extends BasicTargetTasker {
        public AverageHeadingWeighted() {}
        public AverageHeadingWeighted(Collection<? extends VisiblePlayer> targetAgents, Task.Type type) { super(targetAgents, type); }
        public AverageHeadingWeighted(Collection<? extends VisiblePlayer> targetAgents, Type type, double priority) { super(targetAgents, type, priority); }
        @Override public String toString() { return super.toString() + "AverageHeadingWeighted"; }

        public List<Task> generateGiven(DistanceCache dt, Agent owner, Collection<? extends VisiblePlayer> visibleOpponents, Collection<Agent> teammates) {
            if (visibleOpponents.size() == 0)
                return Collections.emptyList();

            Point2D.Double loc = owner.state.position;

            // finds the center of mass
            Point2D.Double avgHeading = new Point2D.Double();
            Point2D.Double heading;
            double dist;
            int num = 0;
            for (VisiblePlayer asp : visibleOpponents) {
                if (! targetAgents.contains(asp) || asp == owner)
                    continue;
                num++;
                if (asp.getVelocity() == null)
                    continue;
                heading = PlanarMathUtils.normalize((Point2D.Double) asp.getVelocity().clone());
                dist = asp.getPosition().distance(loc);
                avgHeading.x += heading.x / dist;
                avgHeading.y += heading.y / dist;
            }
            if (num == 0 || (avgHeading.x == 0  && avgHeading.y == 0))
                return Collections.emptyList();
            avgHeading.x /= num;
            avgHeading.y /= num;

            // return task with phantom agent target
            Point2D.Double tLoc = new Point2D.Double(loc.x + avgHeading.x, loc.y + avgHeading.y);
            return Arrays.asList(new Task(owner, new PhantomAgent(tLoc, PlanarMathUtils.ZERO), priority, type));
        }
    } // INNER CLASS Tasker.AverageHeadingWeighted



    /**
     * Assigns tasks (prey) to n/k pursuers. The algorithm uses the complete table
     * of distances between pursuers and evaders to map the closest
     * pursuer-prey pair. This process is repeated until there are no more
     * pursuers or prey available. If there are leftover pursuers, they are
     * assigned to the closest prey.
     */
    public static class ControlClosest extends BasicTargetTasker {
        public ControlClosest() {}
        public ControlClosest(Collection<? extends VisiblePlayer> targetAgents, Task.Type type) { super(targetAgents, type); }
        public ControlClosest(Collection<? extends VisiblePlayer> targetAgents, Type type, double priority) { super(targetAgents, type, priority); }
        @Override public String toString() { return super.toString() + "ControlClosest"; }

        @Override
        public List<Task> generateGiven(DistanceCache dt, Team owner, Collection<? extends VisiblePlayer> visibleOpponents) {
            if (visibleOpponents.size() == 0)
                return Collections.emptyList();
            
            // reduce to visible opponents only
            HashSet<VisiblePlayer> visOpps = new HashSet<VisiblePlayer>(visibleOpponents);
            visOpps.retainAll(targetAgents);

            Map<VisiblePlayer, VisiblePlayer> assignments = dt.getAssignmentMapByClosestFirst(owner.state.activeAgents, visOpps);
            ArrayList<Task> tasks = new ArrayList<Task>(assignments.size());

            for (Entry<VisiblePlayer, VisiblePlayer> e : assignments.entrySet())
                tasks.add(new Task((Agent) e.getKey(), e.getValue(), priority, type));

            return tasks;
        }
        public List<Task> generateGiven(DistanceCache dt, Agent owner, Collection<? extends VisiblePlayer> visibleOpponents, Collection<Agent> teammates) {
            return Collections.emptyList();
        }
    } // INNER CLASS Tasker.ControlClosest


    /**
     * Assigns tasks (prey) to n/k pursuers, in such a way that the assignment
     * minimizes the total distance between pursuers and targets.
     */
    public static class ControlOptimal extends BasicTargetTasker {
        public ControlOptimal() {}
        public ControlOptimal(Collection<? extends VisiblePlayer> targetAgents, Task.Type type) { super(targetAgents, type); }
        public ControlOptimal(Collection<? extends VisiblePlayer> targetAgents, Type type, double priority) { super(targetAgents, type, priority); }
        @Override public String toString() { return super.toString() + "ControlOptimal"; }

        @Override
        public List<Task> generateGiven(DistanceCache dt, Team owner, Collection<? extends VisiblePlayer> visibleOpponents) {
            // reduce to visible opponents only
            HashSet<VisiblePlayer> visOpps = new HashSet<VisiblePlayer>(visibleOpponents);
            visOpps.retainAll(targetAgents);
            if (visOpps.size() == 0)
                return Collections.emptyList();

            // TODO - implement new logic here
            return Collections.emptyList();
        }
        public List<Task> generateGiven(DistanceCache dt, Agent owner, Collection<? extends VisiblePlayer> visibleOpponents, Collection<Agent> teammates) {
            return Collections.emptyList();
        }
    } // INNER CLASS Tasker.ControlOptimal
}
